/*
 * This file or a portion of this file is licensed under the terms of
 * the Globus Toolkit Public License, found in file GTPL, or at
 * http://www.globus.org/toolkit/download/license.html. This notice must
 * appear in redistributions of this file, with or without modification.
 *
 * Redistributions of this Software, with or without modification, must
 * reproduce the GTPL in: (1) the Software, or (2) the Documentation or
 * some other similar material which is provided with the Software (if
 * any).
 *
 * Copyright 1999-2004 University of Chicago and The University of
 * Southern California. All rights reserved.
 */
package edu.isi.pegasus.planner.parser;

import org.griphyn.vdl.parser.*;
import edu.isi.pegasus.planner.invocation.CPU;
import edu.isi.pegasus.planner.invocation.HasText;
import edu.isi.pegasus.planner.invocation.JobStatus;
import edu.isi.pegasus.planner.invocation.StatCall;
import edu.isi.pegasus.planner.invocation.Architecture;
import edu.isi.pegasus.planner.invocation.Machine;
import edu.isi.pegasus.planner.invocation.StatInfo;
import edu.isi.pegasus.planner.invocation.JobStatusSignal;
import edu.isi.pegasus.planner.invocation.Regular;
import edu.isi.pegasus.planner.invocation.ArgEntry;
import edu.isi.pegasus.planner.invocation.ArgVector;
import edu.isi.pegasus.planner.invocation.Proc;
import edu.isi.pegasus.planner.invocation.Fifo;
import edu.isi.pegasus.planner.invocation.Temporary;
import edu.isi.pegasus.planner.invocation.EnvEntry;
import edu.isi.pegasus.planner.invocation.MachineSpecific;
import edu.isi.pegasus.planner.invocation.JobStatusSuspend;
import edu.isi.pegasus.planner.invocation.Descriptor;
import edu.isi.pegasus.planner.invocation.Task;
import edu.isi.pegasus.planner.invocation.Load;
import edu.isi.pegasus.planner.invocation.Environment;
import edu.isi.pegasus.planner.invocation.Usage;
import edu.isi.pegasus.planner.invocation.Boot;
import edu.isi.pegasus.planner.invocation.MachineInfo;
import edu.isi.pegasus.planner.invocation.Invocation;
import edu.isi.pegasus.planner.invocation.InvocationRecord;
import edu.isi.pegasus.planner.invocation.RAM;
import edu.isi.pegasus.planner.invocation.WorkingDir;
import edu.isi.pegasus.planner.invocation.Arguments;
import edu.isi.pegasus.planner.invocation.JobStatusRegular;
import edu.isi.pegasus.planner.invocation.ArgString;
import edu.isi.pegasus.planner.invocation.Data;
import edu.isi.pegasus.planner.invocation.Stamp;
import edu.isi.pegasus.planner.invocation.Uname;
import edu.isi.pegasus.planner.invocation.Swap;
import edu.isi.pegasus.planner.invocation.Job;
import edu.isi.pegasus.planner.invocation.Status;
import edu.isi.pegasus.planner.invocation.Ignore;
import edu.isi.pegasus.planner.invocation.JobStatusFailure;

import org.griphyn.vdl.util.Logging;

// Xerces
import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;
import java.io.*;
import java.util.*;
import java.text.*;
import java.net.InetAddress;
import java.net.UnknownHostException;

/**
 * This class uses the Xerces SAX2 parser to validate and parse an XML
 * document which contains information from kickstart generated
 * invocation record.
 *
 * @author Jens-S. Vöckler
 * @author Yong Zhao
 * @version $Revision$
 *
 */
public class InvocationParser extends DefaultHandler
{
  /**
   * Default parser is the Xerces parser.
   */
  protected static final String vendorParserClass =
    "org.apache.xerces.parsers.SAXParser";

  /**
   * Holds the instance of a {@link org.xml.sax.XMLReader} class.
   */
  private XMLReader m_parser;

  /**
   * Holds the result, will be overwritten by each invocation of parse().
   */
  private InvocationRecord m_result;

  /**
   * Keep the location within the document
   */
  private Locator m_location;

  /**
   * A Hashmap to forward resolve namespaces that were encountered
   * during parsing.
   */
  private Map m_forward;

  /**
   * A Hashmap to reverse resolve namespaces that were encountered
   * during parsing.
   */
  private Map m_reverse;

  /**
   * Parsing for ISO dates without milliseconds
   */
  private SimpleDateFormat m_coarse;

  /**
   * Parsing for ISO dates with millisecond extension.
   */
  private SimpleDateFormat m_fine;

  /**
   * Obtain our logger once for multiple uses.
   */
  private Logging m_log;

  /**
   * Count the depths of elements in the document
   */
  private int m_depth = 0;

  /**
   * A stack of namespaces?
   */
  private Stack m_stack;

  /**
   * Sets a feature while capturing failed features right here.
   *
   * @param uri is the feature's URI to modify
   * @param flag is the new value to set.
   * @return true, if the feature could be set, false for an exception
   */
  private boolean set( String uri, boolean flag )
  {
    boolean result = false;
    try {
      this.m_parser.setFeature( uri, flag );
      result = true;
    } catch ( SAXException se ) {
      Logging.instance().log( "default", 0,
			      "Could not set parser feature " +
			      se.getMessage() );
    }
    return result;
  }

  /**
   * The class constructor. This function initializes the Xerces parser
   * and the features that enable schema validation.
   *
   * @param schemaLocation is the default location of the XML Schema
   * which this parser is capable of parsing. It may be null to use
   * the defaults provided in the document.
   */
  public InvocationParser( String schemaLocation )
  {
    this.m_forward = new HashMap();
    this.m_reverse = new HashMap();
    this.m_coarse = new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ssZ" );
    this.m_fine = new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ss.SSSZ" );
    this.m_log = Logging.instance();

    try {
      m_parser = (XMLReader) Class.forName(vendorParserClass).newInstance();
      m_parser.setContentHandler(this);
      // m_parser.setErrorHandler(this);
      m_parser.setErrorHandler( new VDLErrorHandler() );

      set( "http://xml.org/sax/features/validation", true );
      set( "http://apache.org/xml/features/validation/dynamic", true );
      set( "http://apache.org/xml/features/validation/schema", true );
      // time+memory consuming, see http://xml.apache.org/xerces2-j/features.html
      // set( "http://apache.org/xml/features/validation/schema-full-checking", true );

      // Send XML Schema element default values via characters().
      set( "http://apache.org/xml/features/validation/schema/element-default", true );
      set( "http://apache.org/xml/features/validation/warn-on-duplicate-attdef", true );
      // mysteriously, this one fails with recent Xerces
      // set( "http://apache.org/xml/features/validation/warn-on-undeclared-elemdef", true );
      set( "http://apache.org/xml/features/warn-on-duplicate-entitydef", true );
      set( "http://apache.org/xml/features/honour-all-schemaLocations", true ); 

      // set the schema default location.
      if ( schemaLocation != null ) {
	setSchemaLocations( InvocationRecord.SCHEMA_NAMESPACE + ' ' +
			    schemaLocation );
	m_log.log("app", 2, "will use " + schemaLocation );
      } else {
	m_log.log("app", 2, "will use document schema hint" );
      }
    } catch (ClassNotFoundException e) {
      m_log.log( "defaut", 0,
		 "The SAXParser class was not found: " + e);
    } catch (InstantiationException e) {
      m_log.log( "default", 0,
		 "The SAXParser class could not be instantiated: " + e);
    } catch (IllegalAccessException e) {
      m_log.log( "default", 0,
		 "The SAXParser class could not be accessed: " + e);
    }
  }

  /**
   * Sets the list of external real locations where the XML schema may
   * be found. Since this list can be determined at run-time through
   * properties etc., we expect this function to be called between
   * instantiating the parser, and using the parser
   *
   * @param list is a list of strings representing schema locations. The
   * content exists in pairs, one of the namespace URI, one of the
   * location URL.
   */
  public void setSchemaLocations( String list )
  {
    // schema location handling
    try {
      m_parser.setProperty(
	"http://apache.org/xml/properties/schema/external-schemaLocation",
	list );
    } catch ( SAXException se ) {
      m_log.log( "default", 0,
		 "The SAXParser reported an error: " + se );
    }
  }

  /**
   * This function parses a XML source from an InputStream source, and
   * creates java class instances that correspond to different elements
   * in the XML source.
   *
   * @param reader is a bytestream opened for reading.
   * @return the records with the invocation information, or null on failure.
   */
  public InvocationRecord parse( java.io.InputStream reader )
  {
    try {
      // will change m_result
      m_parser.parse( new InputSource(reader) );
      return m_result;
    } catch (SAXException e) {
      // e.printStackTrace( System.err );
      m_log.log( "default", 0, "SAX Error: " + e.getMessage() );
    } catch (IOException e) {
      m_log.log( "default", 0, "IO Error: " + e.getMessage() );
    }

    return null;
  }

  /**
   * This function parses a XML source from the new Reader source, and
   * creates java class instances that correspond to different elements
   * in the XML source.
   *
   * @param reader is a character stream opened for reading.
   * @return the records with the invocation information, or null on failure.
   */
  public InvocationRecord parse( java.io.Reader reader )
  {
    try {
      // will change m_result
      m_parser.parse( new InputSource(reader) );
      return m_result;
    } catch (SAXException e) {
      // e.printStackTrace( System.err );
      m_log.log( "default", 0, "SAX Error: " + e.getMessage() );
    } catch (IOException e) {
      m_log.log( "default", 0, "IO Error: " + e.getMessage() );
    }

    return null;
  }

  //
  // here starts the implementation to the Interface
  //

  /**
   * Obtains the document locator from the parser. The document location
   * can be used to print debug information, i.e the current location
   * (line, column) in the document.
   *
   * @param locator is the externally set current position
   */
  public void setDocumentLocator( Locator locator )
  {
    this.m_location = locator;
  }

  private String full_where()
  {
    return ( "line " + m_location.getLineNumber() +
	     ", col " + m_location.getColumnNumber() ); 
  }

  private String where()
  {
    return ( m_location.getLineNumber() + 
	     ":" +
	     m_location.getColumnNumber() ); 
  }

  /**
   * This method specifies what to do when the parser is at the beginning
   * of the document. In this case, we simply print a message for debugging.
   */
  public void startDocument()
  {
    this.m_depth = 0;
    this.m_stack = new Stack();
    this.m_log.log( "parser", 1, "*** start of document ***" );
  }

  /**
   * The parser comes to the end of the document.
   */
  public void endDocument()
  {
    this.m_log.log( "parser", 1, "*** end of document ***" );
  }

  /**
   * There is a prefix or namespace defined, put the prefix and its URI
   * in the HashMap. We can get the URI when the prefix is used here after.
   *
   * @param prefix the Namespace prefix being declared.
   * @param uri the Namespace URI the prefix is mapped to.
   */
  public void startPrefixMapping( java.lang.String prefix,
                                  java.lang.String uri )
    throws SAXException
  {
    String p = prefix == null ? null : new String(prefix);
    String u = uri == null ? null : new String(uri);
    m_log.log( "parser", 2, "adding \"" + p + "\" <=> " + u );

    if ( ! this.m_forward.containsKey(p) )
      this.m_forward.put(p, new Stack());
    ((Stack) this.m_forward.get(p)).push(u);

    if ( ! this.m_reverse.containsKey(u) )
      this.m_reverse.put(u, new Stack());
    ((Stack) this.m_reverse.get(u)).push(p);
  }


  /**
   * Out of the reach of the prefix, remove it from the HashMap.
   *
   * @param prefix is the prefix that was being mapped previously.
   */
  public void endPrefixMapping( java.lang.String prefix )
    throws SAXException
  {
    String u = (String) ((Stack) this.m_forward.get(prefix)).pop();
    String p = (String) ((Stack) this.m_reverse.get(u)).pop();
    m_log.log( "parser", 2, "removed \"" + p + "\" <=> " + u );
  }

  /**
   * Helper function to map prefixes correctly onto the elements.
   *
   * @param uri is the parser-returned URI that needs translation.
   * @return the correct prefix for the URI
   */
  private String map( String uri )
  {
    if ( uri == null || uri.length() == 0 ) return "";
    Stack stack = (Stack) this.m_reverse.get(uri);
    String result = stack == null ? null : (String) stack.peek();
    if ( result == null || result.length() == 0 ) return "";
    else return result + ':';
  }


  /**
   * This method defines the action to take when the parser begins to parse
   * an element.
   *
   * @param namespaceURI is the URI of the namespace for the element
   * @param localName is the element name without namespace
   * @param qName is the element name as it appears in the docment
   * @param atts has the names and values of all the attributes
   */
  public void startElement( java.lang.String namespaceURI,
                            java.lang.String localName,
                            java.lang.String qName,
                            Attributes atts )
    throws SAXException
  {
    m_log.log( "parser", 3,
	       "<" + map(namespaceURI) + localName + "> at " +
	       where() );

    // yup, one more element level
    m_depth++;

    java.util.List names = new java.util.ArrayList();
    java.util.List values = new java.util.ArrayList();
    for ( int i=0; i < atts.getLength(); ++i ) {
      String name = new String( atts.getLocalName(i) );
      String value = new String( atts.getValue(i) );

      m_log.log( "parser", 2, "attribute " + map(atts.getURI(i)) +
		 name + "=\"" + value + "\"" );
      names.add(name);
      values.add(value);
    }

    //System.out.println( "QNAME " + qName + " NAME " + names + "\t Values" + values );
    Invocation parent = null;
    if ( ! m_stack.empty() ) {
        IVSElement peek = (IVSElement) m_stack.peek();
        parent = (Invocation)peek.m_obj;
    }
    Invocation object = createObject( parent, qName, names, values );
    if ( object != null )
      m_stack.push( new IVSElement( qName, object ) );
    else
      throw new SAXException( "empty element while parsing" );
  }

  /**
   * The parser is at the end of an element. Each successfully and
   * completely parsed Definition will trigger a callback to the
   * registered DefinitionHandler.
   *
   * @param namespaceURI is the URI of the namespace for the element
   * @param localName is the element name without namespace
   * @param qName is the element name as it appears in the docment
   */
  public void endElement( java.lang.String namespaceURI,
                          java.lang.String localName,
                          java.lang.String qName )
    throws SAXException
  {
    // that's it for this level
    m_depth--;
    m_log.log( "parser", 3,
	       "</" + map(namespaceURI) + localName + "> at " +
	       where() ); 

    IVSElement tos = (IVSElement) m_stack.pop();
    if ( ! qName.equals(tos.m_name) ) {
      m_log.log( "default", 0, "assertion failure" );
      System.exit(1);
    }

    if ( ! m_stack.empty() ) {
      // add pieces to lower levels
      IVSElement peek = (IVSElement) m_stack.peek();
      if ( !setElementRelation( peek.m_name.charAt(0), peek.m_obj, tos.m_obj )){
	m_log.log( "parser", 0, "Element " + tos.m_name +
		   " does not fit into element " + peek.m_name );
        //System.out.println(  "Element " + tos.m_name +
	//	   " does not fit into element " + peek.m_name );
      }
    } else {
      // run finalizer, if available
      // m_log.log( "default", 0, "How did I get here?" );
    }
  }

  /**
   * This method is the callback function for characters in an element.
   * The element is expected to be of mixed content.
   *
   * @param ch are the characters from the XML document
   * @param start is the start position into the array
   * @param length is the amount of valid data in the array
   */
  public void characters( char[] ch, int start, int length )
    throws SAXException
  {
    String message = new String( ch, start, length );
    if ( message.length() > 0  ) {
      if ( message.trim().length() == 0 )
        m_log.log( "parser", 3, "Characters: \' \' x " +
                                message.length() );
      else
        m_log.log( "parser", 3, "Characters: \"" + message  + "\"" );

      // Insert text into the text carrying elements. These elements
      // must be capable to have text added repeatedly.
      if ( ! m_stack.empty() ) {
	IVSElement tos = (IVSElement) m_stack.peek();
	if ( tos.m_obj instanceof HasText ) {
	  HasText obj = (HasText) tos.m_obj;
	  obj.appendValue(message);
	}
      } else {
	// run finalizer, if available
	m_log.log( "default", 0, "How did I get here II?" );
      }
    }
  }

  /**
   * Currently, ignorable whitespace will be ignored.
   *
   * @param ch are the characters from the XML document
   * @param start is the start position into the array
   * @param length is the amount of valid data in the array
   */
  public void ignorableWhitespace( char[] ch, int start, int length )
    throws SAXException
  {
    // not implemented
  }

  /**
   * Receive a processing instruction. Currently, we are just printing
   * a debug message that we received a PI.
   *
   * @param target the processing instruction target
   * @param data the processing instruction data, or null if none was supplied.
   * The data does not include any whitespace separating it from the target.
   */
  public void processingInstruction( java.lang.String target,
                                     java.lang.String data )
    throws SAXException
  {
    m_log.log( "parser", 2, "processing instruction " + target +
	       "=\"" + data + "\" was skipped!");
  }

  /**
   * Receive a notification that an entity was skipped. Currently, we
   * are just printing a debug message to this fact.
   *
   * @param name The name of the skipped entity. If it is a parameter
   * entity, the name will begin with '%', and if it is the external DTD
   * subset, it will be the string "[dtd]".
   */
  public void skippedEntity(java.lang.String name)
    throws SAXException
  {
    m_log.log( "parser", 2,
	       "entity " + name + " was skipped!");
  }

  //
  // =================================================== our own stuff ===
  //


  /**
   * Small helper method to bundle repetitive parameters in a template
   * for reporting progress.
   *
   * @param subject is the name of the XML element that is being scrutinized.
   * @param name is then name of the element we are working with.
   * @param value is the attribute value.
   */
  private void log( String subject, String name, String value )
  {
    if ( value == null ) value = new String();
    m_log.log( "filler", 3, subject + "." + name + "=\"" +
	       value + "\"" );
  }

  /**
   * Small helper method to bundle repetitive complaints in a template
   * for reporting progress.
   *
   * @param subject is the name of the XML element that is being scrutinized.
   * @param name is then name of the element we are working with.
   * @param value is the attribute value.
   */
  private void complain( String subject, String name, String value )
  {
    if ( value == null ) value = new String();
    m_log.log( "default", 0, "ignoring " + subject + '@' + name +
	       "=\"" + value + '"', true );
  }

  /**
   * Small helper to parse the different date varieties and deal with
   * Java obnoxeity.
   *
   * @param date is an ISO 8601 timestamp
   * @return a date field
   * @exception ParseException thrown if the date cannot be parsed
   */
  private Date parseDate( String date )
    throws ParseException
  {
    // SimpleDataFormat stumbles over colon in time zone
    int size = date.length();
    if ( date.charAt(size-3) == ':' ) {
      StringBuffer temp = new StringBuffer(date);
      temp.deleteCharAt(size-3);
      date = temp.toString();
    }

    Date result;
    if ( date.indexOf('.') == -1 ) {
      // coarse grained timestamp
      result = m_coarse.parse(date);
    } else {
      // fine grained timestamp
      result = m_fine.parse(date);
    }

    SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmssZ");
    m_log.log( "filler", 3, "found date " + sdf.format(result) );
    return result;
  }

  /**
   * Small helper method to set up the attributes for the job elements.
   *
   * @param job is the job to set up.
   * @param names is the list of attribute names
   * @param values is the list of attribute values
   */
  private void setupJob( Job job, java.util.List names, java.util.List values )
    throws NumberFormatException, ParseException
  {
    for ( int i=0; i<names.size(); ++i ) {
      String name = (String) names.get(i);
      String value = (String) values.get(i);

      if ( name.equals("start") ) {
	this.log( job.getTag(), name, value );
	job.setStart( parseDate(value) );
      } else if ( name.equals("duration") ) {
	this.log( job.getTag(), name, value );
	job.setDuration( Double.parseDouble(value) );
      } else if ( name.equals("pid") ) {
	this.log( job.getTag(), name, value );
	job.setPID( (int) (Long.parseLong(value) & 0xFFFFFFFF) );
      } else {
	this.complain( job.getTag(), name, value );
      }
    }
  }

  /**
   * This method determines the actively parsed element, creates the
   * Java object that corresponds to the element, and sets the member
   * variables with the values of the attributes of the element.
   *
   * @param parent  is the parent element
   * @param e is the name of the element
   * @param names  is a list of attribute names, as strings.
   * @param values  is a list of attribute values, to match the key list.
   * @return A new VDL Java object, which may only be partly constructed.
   * @exception IllegalArgumentException if the element name is too short.
   */
  protected Invocation createObject( Invocation parent, 
				     String e, 
				     java.util.List names,
				     java.util.List values )
    throws IllegalArgumentException
  {
    if ( e == null || e.length() < 1 )
      throw new IllegalArgumentException("illegal element length");

    try {
      // postcondition: string has content w/ length > 0
      switch ( e.charAt(0) ) {
	//
	// A
	//
      case 'a':
	if ( e.equals("arg") ) {
	  ArgEntry entry = new ArgEntry();
	  for ( int i=0; i<names.size(); ++i ) {
	    String name = (String) names.get(i);
	    String value = (String) values.get(i);

	    if ( name.equals("nr") ) {
	      this.log( e, name, value );
	      entry.setPosition( Integer.parseInt(value) );
	    } else {
	      this.complain( e, name, value );
	    }
	  }
	  return entry;

	} else if ( e.equals("arguments") ) {
	  Arguments cli = new ArgString();
	  for ( int i=0; i<names.size(); ++i ) {
	    String name = (String) names.get(i);
	    String value = (String) values.get(i);

	    if ( name.equals("executable") ) {
	      this.log( e, name, value );
	      cli.setExecutable(value);
	    } else {
	      this.complain( e, name, value );
	    }
	  }
	  return cli;

	} else if ( e.equals("argument-vector") ) {
	  Arguments cli = new ArgVector();
	  for ( int i=0; i<names.size(); ++i ) {
	    String name = (String) names.get(i);
	    String value = (String) values.get(i);

	    if ( name.equals("executable") ) {
	      this.log( e, name, value );
	      cli.setExecutable(value);
	    } else {
	      this.complain( e, name, value );
	    }
	  }
	  return cli;
	}

	// unknown
	return null;

        //
	// B
	//
      case 'b':
	if ( e.equals("boot") ) {
	  Boot b = new Boot();
	  b.addAttributes(names, values);
	  return b;
        } else if ( e.equals("basic") ) {
	  MachineSpecific basic = new MachineSpecific("basic");
	  return basic; 
	}

	// unknown 
        return null;
        
	//
	// C
	//
      case 'c':
	if ( e.equals("cwd") ) {
	  WorkingDir cwd = new WorkingDir();
	  for ( int i=0; i<names.size(); ++i ) {
	    String name = (String) names.get(i);
	    String value = (String) values.get(i);

	    this.complain( e, name, value );
	  }
	  return cwd;
	} else if ( e.equals("cleanup") ) {
	  Job job = new Job(e);
	  setupJob( job, names, values );
	  return job;
	} else if ( e.equals( "cpu" ) ){
	  CPU c  = new CPU();
	  c.addAttributes(names, values);
	  return c;
        }

	// unknown
	return null;

	//
	// D
	//
      case 'd':
	if ( e.equals("data") ) {
	  Data data = new Data();
	  for ( int i=0; i<names.size(); ++i ) {
	    String name = (String) names.get(i);
	    String value = (String) values.get(i);

	    if ( name.equals("truncated") ) {
	      this.log( e, name, value );
	      data.setTruncated( Boolean.valueOf(value).booleanValue() );
	    } else {
	      this.complain( e, name, value );
	    }
	  }
	  return data;
	} else if ( e.equals("descriptor") ) {
	  Descriptor file = new Descriptor();
	  for ( int i=0; i<names.size(); ++i ) {
	    String name = (String) names.get(i);
	    String value = (String) values.get(i);

	    if ( name.equals("number") ) {
	      this.log( e, name, value );
	      file.setDescriptor( Integer.parseInt(value) );
	    } else {
	      this.complain( e, name, value );
	    }
	  }
	  return file;
	} else if ( e.equals("darwin") ) { 
	  MachineSpecific darwin = new MachineSpecific("darwin");
	  return darwin; 
	}

	// unknown
	return null;

	//
	// E
	//
      case 'e':
	if ( e.equals("env") ) {
	  EnvEntry ee = new EnvEntry();
	  for ( int i=0; i<names.size(); ++i ) {
	    String name = (String) names.get(i);
	    String value = (String) values.get(i);

	    if ( name.equals("key") ) {
	      this.log( e, name, value );
	      ee.setKey( value );
	    } else {
	      this.complain( e, name, value );
	    }
	  }
	  return ee;

	} else if ( e.equals("environment") ) {
	  Environment env = new Environment();
	  for ( int i=0; i<names.size(); ++i ) {
	    String name = (String) names.get(i);
	    String value = (String) values.get(i);
	    this.complain( e, name, value );
	  }
	  return env;
	}

	// unknown
	return null;

	//
	// F
	//
      case 'f':
	if ( e.equals("file") ) {
	  Regular file = new Regular();
	  for ( int i=0; i<names.size(); ++i ) {
	    String name = (String) names.get(i);
	    String value = (String) values.get(i);

	    if ( name.equals("name") ) {
	      this.log( e, name, value );
	      file.setFilename(value);
	    } else {
	      this.complain( e, name, value );
	    }
	  }
	  return file;
	} else if ( e.equals("fifo") ) {
	  Fifo fifo = new Fifo();
	  for ( int i=0; i<names.size(); ++i ) {
	    String name = (String) names.get(i);
	    String value = (String) values.get(i);

	    if ( name.equals("name") ) {
	      this.log( e, name, value );
	      fifo.setFilename(value);
	    } else if ( name.equals("descriptor") ) {
	      this.log( e, name, value );
	      fifo.setDescriptor( Integer.parseInt(value) );
	    } else if ( name.equals("count") ) {
	      this.log( e, name, value );
	      fifo.setCount( Integer.parseInt(value) );
	    } else if ( name.equals("rsize") ) {
	      this.log( e, name, value );
	      fifo.setInputSize( Long.parseLong(value) );
	    } else if ( name.equals("wsize") ) {
	      this.log( e, name, value );
	      fifo.setOutputSize( Long.parseLong(value) );
	    } else {
	      this.complain( e, name, value );
	    }
	  }
	  return fifo;
	} else if ( e.equals("failure") ) {
	  JobStatusFailure failed = new JobStatusFailure();
	  for ( int i=0; i<names.size(); ++i ) {
	    String name = (String) names.get(i);
	    String value = (String) values.get(i);

	    if ( name.equals("error") ) {
	      this.log( e, name, value );
	      failed.setError( Integer.parseInt(value) );
	    } else {
	      this.complain( e, name, value );
	    }
	  }
	  return failed;
	}

	// unknown
	return null;

	//
	// H
	//
      case 'h':
	if ( e.equals("hard") ) {
	  return new Ignore();
	}

	// unknown
	return null;

	//
	// I
	//
      case 'i':
	if ( e.equals("invocation") ) {
	  this.m_result = new InvocationRecord();
	  for ( int i=0; i<names.size(); ++i ) {
	    String name = (String) names.get(i);
	    String value = (String) values.get(i);

	    if ( name.equals("version") ) {
	      this.log( e, name, value );
	      m_result.setVersion( value );
	    } else if ( name.equals("start") ) {
	      this.log( e, name, value );
	      m_result.setStart( parseDate(value) );
	    } else if ( name.equals("duration") ) {
	      this.log( e, name, value );
	      m_result.setDuration( Double.parseDouble(value) );
	    } else if ( name.equals("transformation") ) {
	      this.log( e, name, value );
	      m_result.setTransformation(value);
	    } else if ( name.equals("derivation") ) {
	      this.log( e, name, value );
	      m_result.setDerivation(value);
	    } else if ( name.equals("host") || name.equals("hostaddr") ) {
	      this.log( e, name, value );
	      m_result.setHostAddress( InetAddress.getByName(value) );
	    } else if ( name.equals("hostname") ) {
	      this.log( e, name, value );
	      m_result.setHostname( value );
	    } else if ( name.equals("interface") ) {
	      this.log( e, name, value );
	      m_result.setInterface( value );
	    } else if ( name.equals("resource") ) {
	      this.log( e, name, value );
	      m_result.setResource( value );
	    } else if ( name.equals("ram" ) ) {
	      this.log( e, name, value ); 
	      m_result.setPhysicalMemory( Long.parseLong(value) ); 
	    } else if ( name.equals("pid") ) {
	      this.log( e, name, value );
	      m_result.setPID( (int) (Long.parseLong(value) & 0xFFFFFFFF) ); 
	    } else if ( name.equals("uid") ) {
	      this.log( e, name, value );
	      m_result.setUID( (int) (Long.parseLong(value) & 0xFFFFFFFF) ); 
	    } else if ( name.equals("gid") ) {
	      this.log( e, name, value );
	      m_result.setGID( (int) (Long.parseLong(value) & 0xFFFFFFFF) );
	    } else if ( name.equals("user") ) {
	      this.log( e, name, value );
	      m_result.setUser( value );
	    } else if ( name.equals("group") ) {
	      this.log( e, name, value );
	      m_result.setGroup( value );
	    } else if ( name.equals("schemaLocation") ) {
	      // ignore root element schema location hint
	    } else if ( name.equals("umask") ) {
	      this.log( e, name, value );
	      m_result.setUMask( Integer.parseInt(value,8) );
	    } else if ( name.equals("wf-label") ) {
	      this.log( e, name, value );
	      m_result.setWorkflowLabel( value );
	    } else if ( name.equals("wf-stamp") ) {
	      this.log( e, name, value );
	      m_result.setWorkflowTimestamp( parseDate(value) );
	    } else {
	      this.complain( e, name, value );
	    }
	  }
	  return this.m_result;
	}

	// unknown
	return null;
        
        //
	// L
	//
      case 'l':
	if ( e.equals("load") ) {
	  Load l = new Load();
          l.addAttributes( names, values );
	  return l;
	} else if ( e.equals("linux") ) {
	  MachineSpecific linux = new MachineSpecific("linux"); 
	  return linux; 
	}

	// unknown
	return null;

	//
	// M
	//
      case 'm':
	if ( e.equals("mainjob") ) {
	  Job job = new Job(e);
	  setupJob( job, names, values );
	  return job;
	} else if ( e.equals( "machine" ) ){
	  Machine machine = new Machine();
	  for ( int i=0; i< names.size(); ++i ) {
	    String name = (String) names.get(i);
	    String value = (String) values.get(i);
	    
	    if ( name.equals("page-size") ) {
	      this.log( e, name, value );
	      machine.setPageSize( Long.parseLong(value) ); 
	    } else {
	      this.complain( e, name, value ); 
	    }
	  }
	  return machine;
        }

	// unknown
	return null;

	//
	// P
	//
      case 'p':
	if ( e.equals("prejob") ) {
	  Job job = new Job(e);
	  setupJob( job, names, values );
	  return job;
	} else if ( e.equals("postjob") ) {
	  Job job = new Job(e);
	  setupJob( job, names, values );
	  return job;
	} else if ( e.equals( "proc") ){
	  Proc p = new Proc();
	  p.addAttributes( names, values );
	  return p;
        }

	// unknown
	return null;

	//
	// R
	//
      case 'r':
	if ( e.equals("regular") ) {
	  JobStatusRegular regular = new JobStatusRegular();
	  for ( int i=0; i<names.size(); ++i ) {
	    String name = (String) names.get(i);
	    String value = (String) values.get(i);

	    if ( name.equals("exitcode") ) {
	      this.log( e, name, value );
	      regular.setExitCode( Short.parseShort(value) );
	    } else {
	      this.complain( e, name, value );
	    }
	  }
	  return regular;
	} else if ( e.equals("resource") ) {
	  // ignore
	  return new Ignore();
	} else if( e.equals( "ram" ) ) {
	  RAM r = new RAM();
	  r.addAttributes( names, values );
	  return r;
        } 

	// unknown
	return null;

	//
	// S
	//
      case 's':
	if ( e.equals("statcall") ) {
	  StatCall statcall = new StatCall();
	  for ( int i=0; i<names.size(); ++i ) {
	    String name = (String) names.get(i);
	    String value = (String) values.get(i);

	    if ( name.equals("error") ) {
	      this.log( e, name, value );
	      statcall.setError( Integer.parseInt(value) );
	    } else if ( name.equals("id") ) {
	      this.log( e, name, value );
	      statcall.setHandle(value);
	    } else if ( name.equals("lfn") ) {
	      this.log( e, name, value );
	      statcall.setLFN(value);
	    } else {
	      this.complain( e, name, value );
	    }
	  }
	  return statcall;

	} else if ( e.equals("statinfo") ) {
	  StatInfo statinfo = new StatInfo();
	  for ( int i=0; i<names.size(); ++i ) {
	    String name = (String) names.get(i);
	    String value = (String) values.get(i);
            //System.out.println( name + " -> " + value );
            if ( name.equals("mode") ) {
	      this.log( e, name, value );
	      statinfo.setMode( Integer.parseInt(value,8) );
	    } else if ( name.equals("size") ) {
	      this.log( e, name, value );
	      statinfo.setSize( Long.parseLong(value) );
	    } else if ( name.equals("inode") ) {
	      this.log( e, name, value );
	      statinfo.setINode( (long)Double.parseDouble(value) );
	    } else if ( name.equals("nlink") ) {
	      this.log( e, name, value );
	      statinfo.setLinkCount( Long.parseLong(value) );
	    } else if ( name.equals("blksize") ) {
	      this.log( e, name, value );
	      statinfo.setBlockSize( Long.parseLong(value) );
	    } else if ( name.equals("blocks") ) {
	      this.log( e, name, value );
	      statinfo.setBlocks( Long.parseLong(value) );
	    } else if ( name.equals("atime") ) {
	      this.log( e, name, value );
	      statinfo.setAccessTime( parseDate(value) );
	    } else if ( name.equals("ctime") ) {
	      this.log( e, name, value );
	      statinfo.setCreationTime( parseDate(value) );
	    } else if ( name.equals("mtime") ) {
	      this.log( e, name, value );
	      statinfo.setModificationTime( parseDate(value) );
	    } else if ( name.equals("uid") ) {
	      this.log( e, name, value );
	      statinfo.setUID( (int) (Long.parseLong(value) & 0xFFFFFFFF) );
	    } else if ( name.equals("user") ) {
	      this.log( e, name, value );
	      statinfo.setUser( value );
	    } else if ( name.equals("gid") ) {
	      this.log( e, name, value );
	      statinfo.setGID( (int) (Long.parseLong(value) & 0xFFFFFFFF) );
	    } else if ( name.equals("group") ) {
	      this.log( e, name, value );
	      statinfo.setGroup( value );
	    } else {
	      this.complain( e, name, value );
	    }
	  }
	  return statinfo;

	} else if ( e.equals("status") ) {
	  Status status = new Status();
	  for ( int i=0; i<names.size(); ++i ) {
	    String name = (String) names.get(i);
	    String value = (String) values.get(i);

	    if ( name.equals("raw") ) {
	      this.log( e, name, value );
	      status.setStatus( Integer.parseInt(value) );
	    } else {
	      this.complain( e, name, value );
	    }
	  }
	  return status;

	} else if ( e.equals("soft") ) {
	  return new Ignore();

	} else if ( e.equals("signalled") ) {
	  JobStatusSignal signalled = new JobStatusSignal();
	  for ( int i=0; i<names.size(); ++i ) {
	    String name = (String) names.get(i);
	    String value = (String) values.get(i);

	    if ( name.equals("signal") ) {
	      this.log( e, name, value );
	      signalled.setSignalNumber( Short.parseShort(value) );
	    } else if ( name.equals("corefile") ) {
	      this.log( e, name, value );
	      signalled.setCoreFlag( Boolean.valueOf(value).booleanValue() );
	    } else {
	      this.complain( e, name, value );
	    }
	  }
	  return signalled;

	} else if ( e.equals("suspended") ) {
	  JobStatusSuspend suspended = new JobStatusSuspend();
	  for ( int i=0; i<names.size(); ++i ) {
	    String name = (String) names.get(i);
	    String value = (String) values.get(i);

	    if ( name.equals("signal") ) {
	      this.log( e, name, value );
	      suspended.setSignalNumber( Short.parseShort(value) );
	    } else {
	      this.complain( e, name, value );
	    }
	  }
	  return suspended;

	} else if ( e.equals("setup") ) {
	  Job job = new Job(e);
	  setupJob( job, names, values );
	  return job;
	} else if( e.equals( "stamp" ) ){
	  Stamp s = new Stamp();
	  return s;
        } else if( e.equals( "swap" ) ){
	  Swap s = new Swap();
	  s.addAttributes( names, values );
	  return s;

        } else if ( e.equals("sunos") ) { 
	  MachineSpecific sunos = new MachineSpecific("sunos");
	  return sunos; 
	}

	// unknown
	return null;
 
	//
	// T
	//
      case 't':
	if ( e.equals("temporary") ) {
	  Temporary file = new Temporary();
	  for ( int i=0; i<names.size(); ++i ) {
	    String name = (String) names.get(i);
	    String value = (String) values.get(i);

	    if ( name.equals("name") ) {
	      this.log( e, name, value );
	      file.setFilename(value);
	    } else if ( name.equals("descriptor") ) {
	      this.log( e, name, value );
	      file.setDescriptor( Integer.parseInt(value) );
	    } else {
	      this.complain( e, name, value );
	    }
	  }
	  return file;
	} else if( e.equals( "task" ) ){
	  Task t = new Task();
	  t.addAttributes(names, values);
	  return t;
        } 

	// unknown
	return null;

	//
	// U
	//
      case 'u':
	if ( e.equals("usage") ) {
	  Usage usage = new Usage();
	  for ( int i=0; i<names.size(); ++i ) {
	    String name = (String) names.get(i);
	    String value = (String) values.get(i);

	    if ( name.equals("utime") ) {
	      this.log( e, name, value );
	      usage.setUserTime( Double.parseDouble(value) );
	    } else if ( name.equals("stime") ) {
	      this.log( e, name, value );
	      usage.setSystemTime( Double.parseDouble(value) );
	    } else if ( name.equals("minflt") ) {
	      this.log( e, name, value );
	      usage.setMinorFaults( Integer.parseInt(value) );
	    } else if ( name.equals("majflt") ) {
	      this.log( e, name, value );
	      usage.setMajorFaults( Integer.parseInt(value) );
	    } else if ( name.equals("nswap") ) {
	      this.log( e, name, value );
	      usage.setSwaps( Integer.parseInt(value) );
	    } else if ( name.equals("nsignals") ) {
	      this.log( e, name, value );
	      usage.setSignals( Integer.parseInt(value) );
	    } else if ( name.equals("nvcsw") ) {
	      this.log( e, name, value );
	      usage.setVoluntarySwitches( Integer.parseInt(value) );
	    } else if ( name.equals("nivcsw") ) {
	      this.log( e, name, value );
	      usage.setInvoluntarySwitches( Integer.parseInt(value) );
	    } else if ( name.equals("maxrss") ) {
	      this.log( e, name, value );
	      usage.setMaximumRSS( Integer.parseInt(value) );
	    } else if ( name.equals("ixrss") ) {
	      this.log( e, name, value );
	      usage.setSharedRSS( Integer.parseInt(value) );
	    } else if ( name.equals("idrss") ) {
	      this.log( e, name, value );
	      usage.setUnsharedRSS( Integer.parseInt(value) );
	    } else if ( name.equals("isrss") ) {
	      this.log( e, name, value );
	      usage.setStackRSS( Integer.parseInt(value) );
	    } else if ( name.equals("inblock") ) {
	      this.log( e, name, value );
	      usage.setInputBlocks( Integer.parseInt(value) );
	    } else if ( name.equals("outblock") ) {
	      this.log( e, name, value );
	      usage.setOutputBlocks( Integer.parseInt(value) );
	    } else if ( name.equals("msgsnd") ) {
	      this.log( e, name, value );
	      usage.setSent( Integer.parseInt(value) );
	    } else if ( name.equals("msgrcv") ) {
	      this.log( e, name, value );
	      usage.setReceived( Integer.parseInt(value) );
	    } else {
	      this.complain( e, name, value );
	    }
	  }
	  return usage;
	} else if( e.equals( "uname" ) && parent instanceof Machine ){
	  Uname u = new Uname();
	  u.addAttributes(names, values);
	  return u;
        } else if ( e.equals("uname") ) {
	  Architecture uname = new Architecture();
	  for ( int i=0; i<names.size(); ++i ) {
	    String name = (String) names.get(i);
	    String value = (String) values.get(i);

	    if ( name.equals("system") ) {
	      this.log( e, name, value );
	      uname.setSystemName(value);
	    } else if ( name.equals("archmode") ) {
	      this.log( e, name, value );
	      uname.setArchMode(value);
	    } else if ( name.equals("nodename") ) {
	      this.log( e, name, value );
	      uname.setNodeName(value);
	    } else if ( name.equals("release") ) {
	      this.log( e, name, value );
	      uname.setRelease(value);
	    } else if ( name.equals("machine") ) {
	      this.log( e, name, value );
	      uname.setMachine(value);
	    } else if ( name.equals("domainname") ) {
	      this.log( e, name, value );
	      uname.setDomainName(value);
	    } else {
	      this.complain( e, name, value );
	    }
	  }
	  return uname;
	}
        
	// unknown
	return null;

      default:
	// FIXME: shouldn't this be an exception?
	m_log.log( "filler", 0,
		   "Error: No rules defined for element " + e );
	return null;
      }
    } catch ( NumberFormatException nfe ) {
      m_log.log( "filler", 0,
		 "Error: Unable to parse a number: " + nfe.getMessage() +
		 " at " + where() ); 
      return null;
    } catch ( UnknownHostException uh ) {
      m_log.log( "filler", 0,
		 "Error: Unable to parse a hostname: " + uh.getMessage() +
		 " at " + where() ); 
      return null;
    } catch ( ParseException pe ) {
      m_log.log( "filler", 0,
		 "Error: Unable to parse a date: " + pe.getMessage() +
		 " at " + where() );
      return null;
    }
  }

  /**
   * This method sets the relations between the currently finished XML
   * element and its containing element in terms of Java objects.
   * Usually it involves adding the object to the parent's child object
   * list.
   *
   * @param initial is the first charactor of the parent element name
   * @param parent is a reference to the parent's Java object
   * @param child is the completed child object to connect to the parent
   * @return true if the element was added successfully, false, if the
   * child does not match into the parent.
   */
  protected boolean setElementRelation( char initial,
					Invocation parent,
					Invocation child )
  {
    switch ( initial ) {
      //
      // A
      //
    case 'a':
      if ( parent instanceof ArgVector && child instanceof ArgEntry ) {
	ArgVector args = (ArgVector) parent;
	ArgEntry entry = (ArgEntry) child;
	args.setValue( entry.getPosition(), entry.getValue() );
	return true;
      }
      // unknown
      return false;

      //
      // C
      //
    case 'c':
      if ( parent instanceof Job ) {
	Job job = (Job) parent;
	if ( child instanceof Usage ) {
	  job.setUsage((Usage) child);
	  return true;
	} else if ( child instanceof Status ) {
	  job.setStatus((Status) child);
	  return true;
	} else if ( child instanceof StatCall ) {
	  job.setExecutable((StatCall) child);
	  return true;
	} else if ( child instanceof Arguments ) {
	  job.setArguments((Arguments) child);
	  return true;
	}
      }
      // unknown
      return false;

      //
      // E
      //
    case 'e':
      if ( parent instanceof Environment && child instanceof EnvEntry ) {
	((Environment) parent).addEntry((EnvEntry) child);
	return true;
      }

      // unknown
      return false;

      //
      // I
      //
    case 'i':
      if ( parent instanceof InvocationRecord ) {
	InvocationRecord invocation = (InvocationRecord) parent;
	if ( child instanceof Job ) {
	  invocation.addJob((Job) child);
	  return true;
	} else if ( child instanceof Usage ) {
	  invocation.setUsage((Usage) child);
	  return true;
	} else if ( child instanceof StatCall ) {
	  invocation.addStatCall((StatCall) child);
	  return true;
	} else if ( child instanceof WorkingDir ) {
	  invocation.setWorkingDirectory((WorkingDir) child);
	  return true;
	} else if ( child instanceof Architecture ) {
	  invocation.setArchitecture((Architecture) child);
	  return true;
	} else if ( child instanceof Environment ) {
	  invocation.setEnvironment((Environment) child);
	  return true;
	} else if ( child instanceof Machine ) {
          Machine machine = (Machine) child;
	  invocation.setMachine(machine); 
          
          // convert uname object to Architecture object
          // reqd for Pegasus Bug 39
	  invocation.setArchitecture( machine.getUname().toArchitecture() );
          return true;
	}
      }
      // unknown
      return false;

      //
      // mainjob
      //
    case 'm':
      if ( parent instanceof Job ) {
	Job job = (Job) parent;
	if ( child instanceof Usage ) {
	  job.setUsage((Usage) child);
	  return true;
	} else if ( child instanceof Status ) {
	  job.setStatus((Status) child);
	  return true;
	} else if ( child instanceof StatCall ) {
	  job.setExecutable((StatCall) child);
	  return true;
	} else if ( child instanceof Arguments ) {
	  job.setArguments((Arguments) child);
	  return true;
	}
      } else if ( parent instanceof Machine ) {
	Machine m = (Machine) parent;
	if ( child instanceof Stamp ) {
	  m.setStamp( (Stamp) child );
	  return true; 
	} else if ( child instanceof Uname ) {
	  m.setUname( (Uname) child ); 
	  return true; 
	} else if ( child instanceof MachineSpecific ) {
	  m.setMachineSpecific( (MachineSpecific) child ); 
	  return true; 
	}
      } else if ( parent instanceof MachineSpecific ) {
	MachineSpecific ms = (MachineSpecific) parent;
	if ( child instanceof RAM  ||
	     child instanceof Swap ||
	     child instanceof Boot ||
	     child instanceof CPU  ||
	     child instanceof Load ||
	     child instanceof Proc ||
	     child instanceof Task ) {
	  ms.addMachineInfo( (MachineInfo) child );
	  return true;
	}
      }
      
      // unknown
      return false;

      //
      // P
      //
    case 'p':
      if ( parent instanceof Job ) {
	// both, prejob and postjob
	Job job = (Job) parent;
	if ( child instanceof Usage ) {
	  job.setUsage((Usage) child);
	  return true;
	} else if ( child instanceof Status ) {
	  job.setStatus((Status) child);
	  return true;
	} else if ( child instanceof StatCall ) {
	  job.setExecutable((StatCall) child);
	  return true;
	} else if ( child instanceof Arguments ) {
	  job.setArguments((Arguments) child);
	  return true;
	}
      }

      // unknown
      return false;

      //
      // R
      //
    case 'r':
      if ( parent instanceof Ignore && child instanceof Ignore ) {
	// ignore
	return true;
      }

      // unknown
      return false;

      //
      // S
      //
    case 's':
      if ( parent instanceof Status && child instanceof JobStatus ) {
	((Status) parent).setJobStatus((JobStatus) child);
	return true;
      } else if ( parent instanceof StatCall ) {
	StatCall statcall = (StatCall) parent;
	if ( child instanceof edu.isi.pegasus.planner.invocation.File ) {
	  statcall.setFile((edu.isi.pegasus.planner.invocation.File) child);
	  return true;
	} else if ( child instanceof StatInfo ) {
	  statcall.setStatInfo((StatInfo) child);
	  return true;
	} else if ( child instanceof Data ) {
	  statcall.setData((Data) child);
	  return true;
	}
      } else if ( parent instanceof Job ) {
	// both, prejob and postjob
	Job job = (Job) parent;
	if ( child instanceof Usage ) {
	  job.setUsage((Usage) child);
	  return true;
	} else if ( child instanceof Status ) {
	  job.setStatus((Status) child);
	  return true;
	} else if ( child instanceof StatCall ) {
	  job.setExecutable((StatCall) child);
	  return true;
	} else if ( child instanceof Arguments ) {
	  job.setArguments((Arguments) child);
	  return true;
	}
      }

      // unknown
      return false;

    default:
      // FIXME: shouldn't this be an exception?
      m_log.log( "filler", 0,
		 "Error: unable to join child to parent" );
      return false;
    }
  }
}
